/*
 * Copyright DimYLiK.
 */

package com.example.myapp.server;

import com.example.myapp.client.AntiBaldaService;
import com.example.myapp.client.AntiBaldaView;
import com.example.myapp.shared.SavedSessionModel;
import com.example.myapp.shared.Word;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import static com.example.myapp.client.AntiBaldaView.MAX_LETTERS;
import static com.example.myapp.client.AntiBaldaView.MIN_LETTERS;

@SuppressWarnings("serial")
public class AntiBaldaServiceImpl extends RemoteServiceServlet implements AntiBaldaService {

    private static final String XML_VOCABULARY_PATH = "WEB-INF/vocabulary.xml";
    private static final String ENTRY = "entry";
    private static final String WORD = "word";
    private static final String DESCRIPTION = "descr";
    private static final String SAVED_SESSION_LANGUAGE = "savedLanguage";
    private static final String SAVED_SESSION_POLE_LIST = "savedPoleList";
    private static final String SAVED_SESSION_USED_WORDS = "savedUsedWords";
    private static final String SAVED_SESSION_NON_USED_WORDS = "savedNonUsedWords";

    private static Map<String, String> fullVocabularyMap = new HashMap<String, String>();
    private static Map<Integer, HashSet<String>> vocabularyMap = new HashMap<Integer, HashSet<String>>();

    private static final int POSSIBLE_HOD = 4;
    private static final int[][] hod = new int[][]
            {  // s levogo polja po 4asovoj strelke:  [i, j-1], [i-1, j], [i, j+1], [i+1, j]
                    {0, -1, 0, 1},
                    {-1, 0, 1, 0}
            };

    static {
        readVocabulary();
        updateVocabularyByLength();
    }

    private static void readVocabulary() {
        try {
            System.out.println("Vocabulary reading started");
            fullVocabularyMap.clear();
            DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document document = documentBuilder.parse(new FileInputStream(XML_VOCABULARY_PATH), "UTF-8");
            document.getDocumentElement().normalize();
            NodeList nodeList = document.getElementsByTagName(ENTRY);
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (Node.ELEMENT_NODE == node.getNodeType()) {
                    Element element = (Element) node;
                    String word = getEntryByTag(element, WORD);
                    int len = word.length();
                    if (len <= MAX_LETTERS && len >= MIN_LETTERS) {
                        String description = getEntryByTag(element, DESCRIPTION);
                        fullVocabularyMap.put(word, description);
                    }
                }
            }
            System.out.println("Vocabulary read successfully, " + fullVocabularyMap.size() + " words loaded.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static boolean isWordLenCorrect(int len, int minLetters, int maxLetters) {
        return len <= maxLetters && len >= minLetters;
    }

    private static void updateVocabularyByLength() {
        System.out.println("Vocabulary sorting by length reading started");
        vocabularyMap.clear();
        for (String word : fullVocabularyMap.keySet()) {
            int len = word.length();
            HashSet<String> words;
            if (vocabularyMap.containsKey(len)) {
                words = vocabularyMap.get(len);
            } else {
                words = new HashSet<String>();
            }
            words.add(word);
            vocabularyMap.put(len, words);
        }
        System.out.println("Vocabulary sorting by length finished successfully");
    }


    public Integer initVocabulary() {
        if (fullVocabularyMap.size() > 0 && vocabularyMap.size() > 0) {
            return fullVocabularyMap.size();
        }
        return null;
    }

    private static String getEntryByTag(Element fstElmnt, String tag) {
        NodeList nodeList = fstElmnt.getElementsByTagName(tag);
        Element element = (Element) nodeList.item(0);
        NodeList childNodes = element.getChildNodes();
        return childNodes.item(0).getNodeValue();
    }

    public List<Word> findWords(SavedSessionModel savedSessionModel,
                                Integer minLetters, Integer maxLetters, Integer maxWords) {
        try {
            saveToSession(savedSessionModel);
            List<Word> possibleWords = calculatePossibleWords(savedSessionModel, minLetters, maxLetters);
            Map<Integer, HashSet<Word>> possibleWordsMap = optimizeSequence(possibleWords);
            Set<Word> userWordsSet = getUsedWordsSet(savedSessionModel);
            possibleWords = findPossibleWords(possibleWordsMap, userWordsSet, maxWords);
            return possibleWords;
        } catch (Exception e) {
            return new ArrayList<Word>();
        }
    }

    private void saveToSession(SavedSessionModel savedSessionModel) {
        HttpServletRequest threadLocalRequest = getThreadLocalRequest();
        if (threadLocalRequest != null) {
            HttpSession session = threadLocalRequest.getSession();
            if (session != null) {
                List<List<String>> poleList = savedSessionModel.getPoleList();
                List<Word> usedWords = savedSessionModel.getUsedWords();
                List<Word> nonUsedWords = savedSessionModel.getNonUsedWords();
                session.setAttribute(SAVED_SESSION_POLE_LIST, poleList);
                session.setAttribute(SAVED_SESSION_USED_WORDS, usedWords);
                session.setAttribute(SAVED_SESSION_NON_USED_WORDS, nonUsedWords);
            }
        }
    }

    private List<Word> findPossibleWords(Map<Integer, HashSet<Word>> possibleWordsMap, Set<Word> usedWords, int maxResult) {
        List<Word> possibleWords = new ArrayList<Word>(find(possibleWordsMap, usedWords, maxResult));
        Collections.sort(possibleWords);
        return possibleWords;
    }

    private List<Word> find(Map<Integer, HashSet<Word>> possibleWordsMap, final Set<Word> usedWords, int maxResult) {
        Set<Word> result = new HashSet<Word>();
        List<Integer> wordLenList = new ArrayList<Integer>(possibleWordsMap.keySet());
        Collections.sort(wordLenList, new IntegerDescendingComparator());
        for (Integer len : wordLenList) {
            Set<Word> wordSequence = possibleWordsMap.get(len);
            if (wordSequence != null) {
                Set<String> vocabularyWords = vocabularyMap.get(len);
                if (vocabularyWords != null) {
                    List<String> fullRegexPatterns = createFullRegex(wordSequence);
                    if (!fullRegexPatterns.isEmpty()) {
                        for (String vocabulary : vocabularyWords) {
                            if (isRegexMatches(vocabulary, fullRegexPatterns)) {
                                if (!usedWords.contains(new Word(vocabulary))) {
                                    Word sequence = getWordSequenceByPattern(wordSequence, vocabulary);
                                    String move = sequence.getMove();
                                    result.add(new Word(vocabulary, move));
                                    if (result.size() >= maxResult) {
                                        return new ArrayList<Word>(result);
                                    }
                                }
                            }
                        }
                    }
                }
            }
//            if (!result.isEmpty()) {
//                return new ArrayList<Word>(result);
//            }
        }
        return new ArrayList<Word>(result);
    }

    private boolean isRegexMatches(String vocabulary, List<String> fullRegexPatterns) {
        for (String regex : fullRegexPatterns) {
            Pattern fullWordPattern = Pattern.compile(regex);
            if (fullWordPattern.matcher(vocabulary).matches()) {
                return true;
            }
        }
        return false;
    }

    private Word getWordSequenceByPattern(Set<Word> wordSequence, String vocabulary) {
        for (Word sequence : wordSequence) {  // optimize to get matched word move
            Pattern oneWordPattern = Pattern.compile(sequence.getWord());
            if (oneWordPattern.matcher(vocabulary).matches()) {
                return sequence;
            }
        }
        throw new UnsupportedOperationException("Error with regexp logic...");
    }

    private List<String> createFullRegex(Set<Word> words) {
        // test on  "myregexp . com"
        StringBuilder builder = new StringBuilder("");
        List<String> regexPaterns = new ArrayList<String>();
        for (Iterator<Word> iterator = words.iterator(); iterator.hasNext();) {
            Word word = iterator.next();
            builder.append(word.getWord());
            if (builder.length() > 100) {
                regexPaterns.add(builder.toString());
                builder = new StringBuilder("");
            } else if (iterator.hasNext()) {
                builder.append("|");
            }
        }
        if (builder.length() > 0) {
            regexPaterns.add(builder.toString());
        }
        return regexPaterns;
    }

    private List<Word> calculatePossibleWords(SavedSessionModel savedSessionModel, Integer minLetters, Integer maxLetters) {
        List<List<String>> poleList = savedSessionModel.getPoleList();
        Set<Word> possibleWords = new HashSet<Word>();
        for (int i = 0; i < AntiBaldaView.POLE_SIZE; i++) {
            for (int j = 0; j < AntiBaldaView.POLE_SIZE; j++) {
                mainCycle(i, j, poleList, possibleWords, minLetters, maxLetters);
            }
        }
        return new ArrayList<Word>(possibleWords);
    }

    private void mainCycle(int i, int j,
                           List<List<String>> poleList, Set<Word> possibleWords,
                           Integer minLetters, Integer maxLetters) {
        String moves = Word.logCurrentMove(i, j);
        String possibleWord = poleList.get(i).get(j);
        fullCycle(i, j, possibleWord, moves, possibleWords, poleList, minLetters, maxLetters);
    }

    private void fullCycle(int i, int j,
                           String possibleWord, String moves,
                           Set<Word> possibleWords,
                           List<List<String>> poleList,
                           Integer minLetters, Integer maxLetters) {
        for (int x = 0; x < POSSIBLE_HOD; x++) {
            int new_i = i + hod[0][x];
            int new_j = j + hod[1][x];
            if (new_i > -1 && new_i < AntiBaldaView.POLE_SIZE && new_j > -1 && new_j < AntiBaldaView.POLE_SIZE) {
                String currentMove = Word.logCurrentMove(new_i, new_j);
                String letter = poleList.get(new_i).get(new_j);
                if (possibleWord.indexOf(AntiBaldaView.X + AntiBaldaView.X) > -1) {
                    addToPossibleWordsList(possibleWord, moves, possibleWords, minLetters, maxLetters);
                } else if (moves.indexOf(currentMove) > -1) {
                    addToPossibleWordsList(possibleWord, moves, possibleWords, minLetters, maxLetters);
                } else if (AntiBaldaView.X.equals(letter) && possibleWord.indexOf(AntiBaldaView.X) > -1) {
                    addToPossibleWordsList(possibleWord, moves, possibleWords, minLetters, maxLetters);
                } else {
                    addToPossibleWordsList(possibleWord + letter, moves + "-" + currentMove, possibleWords, minLetters, maxLetters);
                    fullCycle(new_i, new_j, possibleWord + letter, moves + "-" + currentMove, possibleWords, poleList, minLetters, maxLetters);
                }
            }
        }
        addToPossibleWordsList(possibleWord, moves, possibleWords, minLetters, maxLetters);
    }

    private void addToPossibleWordsList(String possibleWord, String moves, Set<Word> possibleWords,
                                        Integer minLetters, Integer maxLetters) {
        if (possibleWord.indexOf(AntiBaldaView.X) > -1 && isWordLenCorrect(possibleWord.length(), minLetters, maxLetters)) {
            possibleWords.add(new Word(possibleWord, moves));
        }
    }

    private Map<Integer, HashSet<Word>> optimizeSequence(List<Word> possibleWords) {
        possibleWords.remove(Word.WORD_X);
        Map<Integer, HashSet<Word>> possibleWordsMap = new HashMap<Integer, HashSet<Word>>();
        for (Word possibleWord : possibleWords) {
            String word = possibleWord.getWord();
            Integer len = word.length();
            HashSet<Word> wordsByLenSet;
            if (possibleWordsMap.containsKey(len)) {
                wordsByLenSet = possibleWordsMap.get(len);
            } else {
                wordsByLenSet = new HashSet<Word>();
            }
            wordsByLenSet.add(possibleWord);
            possibleWordsMap.put(len, wordsByLenSet);
        }
        return possibleWordsMap;
    }

    private HashSet<Word> getUsedWordsSet(SavedSessionModel savedSessionModel) {
        List<Word> usedWords = savedSessionModel.getUsedWords();
        List<Word> nonUsedWords = savedSessionModel.getNonUsedWords();

        HashSet<Word> usedWordsSet = new HashSet<Word>();
        usedWordsSet.addAll(usedWords);
        usedWordsSet.addAll(nonUsedWords);
        return usedWordsSet;
    }

    public SavedSessionModel checkSession() {
        HttpSession session = getThreadLocalRequest().getSession(false);
        if (session != null) {
            Object savedPoleList = session.getAttribute(SAVED_SESSION_POLE_LIST);
            if (savedPoleList != null && savedPoleList instanceof List) {
                Object savedUsedWords = session.getAttribute(SAVED_SESSION_USED_WORDS);
                if (savedUsedWords != null && savedUsedWords instanceof List) {
                    Object savedNonUsedWords = session.getAttribute(SAVED_SESSION_NON_USED_WORDS);
                    if (savedNonUsedWords != null && savedNonUsedWords instanceof List) {
                        return new SavedSessionModel((List<List<String>>) savedPoleList, (List<Word>) savedUsedWords, (List<Word>) savedNonUsedWords);
                    }
                }
            }
        }
        return null;
    }

    public String getWordDescription(String stringWord) {
        if (stringWord != null) {
            return fullVocabularyMap.get(stringWord);
        }
        return null;
    }

    public String getSavedLanguage(String defaultLanguage) {
        HttpSession session = getThreadLocalRequest().getSession();
        if (session != null) {
            Object savedLanguage = session.getAttribute(SAVED_SESSION_LANGUAGE);
            if (savedLanguage != null && savedLanguage instanceof String) {
                return (String) savedLanguage;
            } else {
                session.setAttribute(SAVED_SESSION_LANGUAGE, defaultLanguage);
                return defaultLanguage;
            }
        }
        return defaultLanguage;
    }

    public boolean saveLanguage(String language) {
        HttpSession session = getThreadLocalRequest().getSession();
        if (session != null) {
            session.setAttribute(SAVED_SESSION_LANGUAGE, language);
            return true;
        }
        return false;
    }

    public String initIpAddress() {
        String clientIP = getThreadLocalRequest().getRemoteAddr();
        System.out.println("Client with IP '" + clientIP + "' entered");
        return clientIP;
    }
}
